/*
 * Copyright 2009 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.beepmystuff;

import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;

/**
 * Network handling service for the various activities.
 *
 * @see NetworkThread
 */
public class NetworkService extends Service {
  private static final String TAG = "BMS.NetworkService";
  private Preferences preferences;
  private BmsClient bmsClient;
  private ScanRegistry scanRegistry;
  private NetworkThread thread;

  private final IBinder binder = new LocalBinder();

  /**
   * A class to implement the Binder interface.
   * Because we're running in the same process as our clients, we can ignore
   * IDL and everything related to IPC.
   */
  private class LocalBinder extends Binder {
    // Just provide a single accessor method that can return the service
    // instance.
    public NetworkService getService() {
      return NetworkService.this;
    }
  }

  /**
   * Class used to bind to a NetworkService instance.
   *
   * This is an implementation of ServiceConnection that uses a local binder
   * to retrieve the original instance of the NetworkService class, then makes
   * it available via a getter.
   * 
   * @see NetworkService#bind(Context, NetworkServiceConnection, Runnable)
   */
  public static class NetworkServiceConnection implements ServiceConnection {
    private Runnable runnable = null;
    private NetworkService networkService = null;

    private void setRunnable(Runnable runnable) {
      if (this.runnable != null || networkService != null) {
        throw new IllegalStateException(
            "This NetworkServiceConnection has already been used to bind to a service.");
      }
      this.runnable = runnable;
    }

    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
      Log.d(TAG, "Service connected");
      // Just cast directly and access the service itself - it's in-process.
      networkService = ((LocalBinder) service).getService();
      runnable.run();
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
      Log.d(TAG, "Service disconnected");
      // It's crashed or (perhaps?) the client disconnected.
      // The former should be impossible, since it's in-process.
      networkService = null;
    }

    /**
     * Returns the network service instance.  Null until the runnable
     * passed to {@link NetworkService#bind(Context, NetworkServiceConnection, Runnable)}
     * has been called, and null after the service has been disconnected.
     */
    public NetworkService getService() {
      return networkService;
    }
  }

  @Override
  public IBinder onBind(Intent intent) {
    return binder;
  }

  /**
   * Convenience method to bind a context to an instance of this service,
   * starting it if necessary.
   *
   * The Runnable passed here will be called once the service has started, at
   * which point {@link NetworkServiceConnection#getService()} will return a
   * non-null value.
   *
   * Callers must unbind by passing the {@code connection} to
   * {@link Context#unbindService(ServiceConnection)}.
   *
   * @param context the activity's context
   * @param connection the {@link NetworkServiceConnection} for this activity
   * @param runnable the Runnable to call once the service has started
   */
  public static void bind(Context context, NetworkServiceConnection connection, Runnable runnable) {
    // The service will be started asynchronously in the current thread - so
    // we can't block here until it's started, because that would block the
    // startup we're waiting for! (I suspect that it's started in response to a
    // message arriving at the UI thread's message queue.)  Instead, we call a
    // Runnable once bound.
    connection.setRunnable(runnable);
    if (!context.bindService(new Intent(context, NetworkService.class), connection, BIND_AUTO_CREATE)) {
      Log.e(TAG, "Couldn't start local service?");
      throw new AssertionError("fail.");
    }
  }

  @Override
  public void onCreate() {
    super.onCreate();
    // Create and initialise the BeepMyStuff API and the ScanRegistry.
    Injector injector = Injector.getInjector(getApplication());
    preferences = injector.getPreferences();
    String nickname = preferences.getNickname();
    String password = preferences.getPassword();
    bmsClient = new BmsClient(injector.getBmsLowLevelClient(), injector.getSessionStorage());
    if (!nickname.equals("") && !password.equals("")) {
      bmsClient.setCredentials(nickname, password);
    }
    scanRegistry = new ScanRegistry(getApplicationContext());
    Log.d(TAG, "Starting background thread");
    BmsHttpClient httpClient = injector.getBmsHttpClient();
    thread = new NetworkThread(getApplicationContext(), httpClient, bmsClient, scanRegistry);
    thread.start();
  }

  @Override
  public void onDestroy() {
    thread.shutdown();
    super.onDestroy();
  }

  // Methods called by our clients.

  /** Returns the scan registry in use. */
  public ScanRegistry getScanRegistry() {
    return scanRegistry;
  }

  /** Provides the API client with new credentials. */
  public void setCredentials(String nickname, String password) {
    bmsClient.setCredentials(nickname, password);
  }

  /** Wakes up the workqueue to look for more work. */
  public void wakeUp() {
    thread.wakeUp();
  }

  /**
   * Log a user in to to Beep My Stuff to get a valid session token.
   *
   * Note that this is a synchronous activity - it is not performed on the
   * network thread.
   *
   * @param nickname the nickname of the user who wants to login
   * @param password the API password for the user
   *
   * @throws BmsTransportException if the request could not be made, or if the
   *     response was unexpected or unintelligible.
   * @throws BmsApiException if an application-level error occurred.
   */
  public void login(String nickname, String password) throws BmsApiException, BmsTransportException {
    bmsClient.login(nickname, password);
  }
}
